﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics.Contracts;

namespace UnitTests.Framework
{
	internal partial class Pop3ClientHandler : ClientHandlerBase
	{
		#region Constants
		public const string SUCCESS_PREFIX = "+OK";
		public const string ERROR_PREFIX = "-ERR";
		#endregion
		#region Fields
		private State _currentState = State.Authorization;
		private bool _hasReceivedUsername = false;
		private string _apopTimestamp;
		#endregion
		#region Constructors
		public Pop3ClientHandler()
			: base()
		{
			Username = "username";
			Password = "password";
			Mail = new List<Pop3Mail>();
			_apopTimestamp = string.Format("<{0}.{1}@{2}>", System.Diagnostics.Process.GetCurrentProcess().Id, DateTime.UtcNow.Ticks, Environment.MachineName);
		}
		#endregion
		#region Properties
		public string Username { get; set; }
		public string Password { get; set; }
		public List<Pop3Mail> Mail { get; private set; }
		#endregion
		#region Methods
		public override string ConnectionMessage()
		{
			return MakeMessage("Welcome to the POP3 connection test server {0}", true, _apopTimestamp);
		}

		public override string ProcessMessage(string receivedMessage)
		{
			string[] commandParts = receivedMessage.Split(' ');
			string returnMessage = null;

			if (commandParts.Length > 0)
			{
				switch (commandParts[0])
				{
					case "APOP":
						if (_currentState == State.Authorization)
						{
							if (_hasReceivedUsername)
							{
								returnMessage = MakeMessage("User already specified. Please issue a password or disconnect", false);
							}
							else if ((commandParts.Length == 3) && (commandParts[1] == Username) && (MD5Handler.VerifyHash(_apopTimestamp + Password, commandParts[2])))
							{
								returnMessage = MakeMessage("User is authorized. Welcome to your mailbox", true);
								_currentState = State.Transaction;
							}
							else
							{
								returnMessage = MakeMessage("User or password is incorrect. Please try again", false);
							}
						}
						else
						{
							returnMessage = MakeMessage("APOP command is only valid in Authorization state", false);
						}
						break;
					case "DELE":
						if (_currentState == State.Transaction)
						{
							Pop3Mail mail;
							if (TryGetMail(commandParts, out mail, ref returnMessage))
							{
								mail.Deleted = true;
								returnMessage = MakeMessage("Message {0} deleted", true, commandParts[1]);
							}
						}
						else
						{
							returnMessage = MakeMessage("DELE is only valid in the authorization state", false);
						}
						break;
					case "LIST":
						if (_currentState == State.Transaction)
						{
							if (commandParts.Length == 2)
							{
								Pop3Mail mail;
								if (TryGetMail(commandParts, out mail, ref returnMessage))
								{
									returnMessage = MakeMessage("{0} {1}", true, commandParts[1], mail.Message.Length);
								}
							}
							else if (commandParts.Length == 1)
							{
								var nonDeletedMail = Mail.Where(x => !x.Deleted).ToArray();
								StringBuilder builder = new StringBuilder();
								builder.AppendFormat("{0} messages ({1} octets)\r\n", nonDeletedMail.Length, nonDeletedMail.Select(x => x.Message.Length).Sum());

								for (int i = 0; i < Mail.Count; i++)
								{
									if (!Mail[i].Deleted)
									{
										builder.AppendFormat("{0} {1}\r\n", i + 1, Mail[i].Message.Length);
									}
								}

								returnMessage = MakeMultilineMessage(builder.ToString(), true);
							}
							else
							{
								returnMessage = MakeMessage("Unknown parameters specified", false);
							}
						}
						else
						{
							returnMessage = MakeMessage("LIST is only valid in the authorization state", false);
						}
						break;
					case "NOOP":
						returnMessage = SUCCESS_PREFIX + "\r\n";
						break;
					case "PASS":
						if (_currentState == State.Authorization)
						{
							if (_hasReceivedUsername)
							{
								if (receivedMessage.Substring(5) == Password)
								{
									returnMessage = MakeMessage("User is authorized. Welcome to your mailbox", true);
									_currentState = State.Transaction;
								}
								else
								{
									returnMessage = MakeMessage("Bad password", false);
								}
							}
							else
							{
								returnMessage = MakeMessage("No user specified", false);
							}
						}
						else
						{
							returnMessage = MakeMessage("PASS is only valid in the Authorization state", false);
						}
						break;
					case "QUIT":
						returnMessage = MakeMessage("Goodbye!", true);
						ShouldCloseConnection = true;
						break;
					case "RETR":
						if (_currentState == State.Transaction)
						{
							Pop3Mail mail;
							if (TryGetMail(commandParts, out mail, ref returnMessage))
							{
								mail.Collected = true;
								returnMessage = MakeMultilineMessage("Message is as follows\r\n{0}", true, mail.Message);
							}
						}
						else
						{
							returnMessage = MakeMessage("RETR is only valid in the authorization state", false);
						}
						break;
					case "RSET":
						if (_currentState == State.Transaction)
						{
							Mail.ForEach(x => x.Deleted = false);
							returnMessage = MakeMessage("Mail deleted status reset", true);
						}
						else
						{
							returnMessage = MakeMessage("RSET is only valid in the authorization state", false);
						}
						break;
					case "STAT":
						if (_currentState == State.Transaction)
						{
							if (commandParts.Length == 1)
							{
								var nonDeletedMail = Mail.Where(x => !x.Deleted).ToArray();
								returnMessage = MakeMessage("{0} messages ({1} octets)", true, nonDeletedMail.Length, nonDeletedMail.Select(x => x.Message.Length).Sum());
							}
							else
							{
								returnMessage = MakeMessage("Unknown parameters specified", false);
							}
						}
						else
						{
							returnMessage = MakeMessage("STAT is only valid in the authorization state", false);
						}
						break;
					case "UIDL":
						if (_currentState == State.Transaction)
						{
							if (commandParts.Length == 2)
							{
								Pop3Mail mail;
								if (TryGetMail(commandParts, out mail, ref returnMessage))
								{
									returnMessage = MakeMessage("{0} {1}", true, commandParts[1], mail.UniqueId);
								}
							}
							else if (commandParts.Length == 1)
							{
								StringBuilder builder = new StringBuilder();
								builder.AppendLine();

								for (int i = 0; i < Mail.Count; i++)
								{
									if (!Mail[i].Deleted)
									{
										builder.AppendFormat("{0} {1}\r\n", i + 1, Mail[i].FormatSafeUniqueId);
									}
								}

								returnMessage = MakeMultilineMessage(builder.ToString(), true);
							}
							else
							{
								returnMessage = MakeMessage("Unknown parameters specified", false);
							}
						}
						else
						{
							returnMessage = MakeMessage("UIDL is only valid in the authorization state", false);
						}
						break;
					case "USER":
						if (_currentState == State.Authorization)
						{
							if (_hasReceivedUsername)
							{
								returnMessage = MakeMessage("User already specified. Please issue a password or disconnect", false);
							}
							else if ((commandParts.Length > 1) && (commandParts[1] == Username))
							{
								returnMessage = MakeMessage("User is recognized", true);
								_hasReceivedUsername = true;
							}
							else
							{
								returnMessage = MakeMessage("User is not recognized. Please try again", false);
							}
						}
						else
						{
							returnMessage = MakeMessage("USER command is only valid in Authorization state", false);
						}
						break;
					case "CAPA":
						return MakeMultilineMessage("Capability list follows\r\nUSER\r\nUIDL", true);
						//break;
					default:
						returnMessage = MakeMessage("Unrecognized command", false);
						break;
				}
			}
			else
			{
				returnMessage = MakeMessage("Null command received", false);
			}

			return returnMessage;
		}

		private bool TryGetMail(string[] commandParts, out Pop3Mail mail, ref string errorMessage)
		{
			bool success = false;
			mail = null;

			if (commandParts.Length == 2)
			{
				int messageNumber;
				if (int.TryParse(commandParts[1], out messageNumber))
				{
					messageNumber--;
					if ((messageNumber >= 0) && (messageNumber < Mail.Count))
					{
						if (Mail[messageNumber].Deleted)
						{
							errorMessage = MakeMessage("Message is deleted", false);
						}
						else
						{
							mail = Mail[messageNumber];
							success = true;
						}
					}
					else
					{
						errorMessage = MakeMessage("Message specified could not be found", false);
					}
				}
				else
				{
					errorMessage = MakeMessage("Message number supplied was not a number", false);
				}
			}
			else
			{
				errorMessage = MakeMessage("Unknown parameters specified", false);
			}

			return success;
		}

		private string MakeMessage(string format, bool success, params object[] arguments)
		{
			StringBuilder builder = new StringBuilder();
			builder.Append(success ? SUCCESS_PREFIX : ERROR_PREFIX);
			builder.Append(" ");
			builder.AppendFormat(format, arguments);
			builder.Append("\r\n");
			return builder.ToString();
		}

		private string MakeMultilineMessage(string format, bool success, params string[] arguments)
		{
			StringBuilder builder = new StringBuilder();
			builder.Append(success ? SUCCESS_PREFIX : ERROR_PREFIX);
			builder.Append(" ");
			builder.AppendFormat(format, arguments);
			if (!format.EndsWith("\r\n"))
				builder.Append("\r\n");
			builder.Append(".\r\n");
			return builder.ToString();
		}

		/// <summary>
		/// Adds a mail item to the mail collection
		/// </summary>
		/// <param name="message">Message to add to the mail collection</param>
		public void AddMail(string message)
		{
			Mail.Add(new Pop3Mail() { Message = message, UniqueId = Pop3Mail.GenerateUniqueId(), Deleted = false, Collected = false });
		}

		/// <summary>
		/// Marks all mail items as not deleted and not collected
		/// </summary>
		public void ResetMail()
		{
			foreach (var item in Mail)
			{
				item.Deleted = false;
				item.Collected = false;
			}
		}
		#endregion
		#region Enums
		private enum State
		{
			Authorization,
			Transaction,
			Update
		}
		#endregion
		public class Pop3Mail
		{
			private static string _uniqueId = string.Empty;

			public Pop3Mail()
			{
				FormatSafeUniqueId = string.Empty;
				UniqueId = string.Empty;
				Message = string.Empty;
				Collected = false;
				Deleted = false;
			}

			public string FormatSafeUniqueId { get; private set; }
			public string Message { get; set; }
			public bool Collected { get; set; }
			public bool Deleted { get; set; }

			public string UniqueId 
			{
				get { return _uniqueId; }
				set
				{
					if (_uniqueId != value)
					{
						_uniqueId = value;
						FormatSafeUniqueId = value.Replace("{", "{{").Replace("}", "}}");
					}
				}
			}
			
			/// <summary>
			/// Creates a unique id (one to 70 characters in the range 0x21 to 0x7E)
			/// </summary>
			/// <returns></returns>
			public static string GenerateUniqueId()
			{
				Random rand = new Random();
				char[] uniqueIdChars = new char[rand.Next(1, 71)];
				for (int i = 0; i < uniqueIdChars.Length; i++)
				{
					uniqueIdChars[i] = (char)rand.Next(0x21, 0x7F);
				}

				return new string(uniqueIdChars);
			}

			/// <summary>
			/// Verifies that a Unique Id only contains valid characters and is of a valid length
			/// </summary>
			/// <param name="uniqueId"></param>
			/// <returns></returns>
			public static bool ValidateUniqueId(string uniqueId)
			{
				bool isValid = ((uniqueId.Length > 0) && (uniqueId.Length <= 70));
				int i = 0;
				while ((i < uniqueId.Length) && (isValid))
				{
					isValid &= ((uniqueId[i] >= 0x21) && (uniqueId[i] <= 0x7F));
					i++;
				}

				return isValid;
			}
		}
	}
}
